01|WebKit 自己都承认:这件事在 CSS 世界里讨论了二十多年
:has() 真正扎人的地方,不是它终于来了。
而是它来得实在太晚。
WebKit 在 2022 年发布 :has() 支持说明时,开头几乎是摊牌式地回顾这段历史:
过去 二十多年,开发者反复去找 CSS Working Group,要求想办法给出一个“父选择器”。
需求很明确。
语法也不是想不出来。
真正卡住大家的,是另一句更冷的话:
浏览器得先搞清楚,面对可能非常复杂的循环模式,这东西到底能不能算得够快。
这一下,问题就不再是“为什么规范不体贴开发者”。
而变成了:
为什么一个大家想了二十年的直觉功能,会让浏览器引擎长期觉得自己背不起账。
这才是 :has() 最值得写的地方。
02|很多前端第一次看到它,心里冒出来的不是惊喜,而是怨气
这情绪太正常了。
因为 :has() 对前端来说,实在太像一个“早就该有”的能力。
你想根据子元素状态改父元素样式。
你想根据某个后代是否存在,决定当前容器怎么排版。
你想写出“如果里面有图片,就给外层另一套样式”“如果表单某处报错,就把整个块提亮”“如果卡片里出现某种内容,就让布局切换”的规则。
这些需求一点也不怪。
甚至可以说,它们太正常了。
正常到你会觉得:
CSS 怎么会拖到 2022 年,才终于让这类写法开始大规模进主流?
它不是“一个冷门能力终于发布”。
它是 CSS 世界里最著名的长期怨念之一,甚至可以说,是很多前端对“浏览器为什么总不肯给我顺手工具”这件事最集中的情绪投射。
03|表面上大家想要的是“父选择器”,实际上浏览器害怕的是“反向追责”
:has() 最常被大众理解成“父选择器终于来了”。
这句话传播上没问题。
但如果你只停在这层,就会低估浏览器为什么这么怕它。
因为 CSS 选择器系统长期有一种相对稳定的工作方式:
我去看这个元素自己长什么样,再看它和祖先、兄弟、状态之间的关系,然后决定它匹不匹配某条规则。
这套思路不是说永远简单。
但它大体上还是一种“我根据目标元素附近已知信息做匹配”的模型。
:has() 一进来,味道就变了。
因为它允许你写出这种逻辑:
这个元素自己的样式,要取决于它里面有没有某种后代、某种状态、某种结构。
一旦这样,浏览器面对的不再只是“当前元素该不该吃这条规则”。
它还得不断担心另一件事:
如果后代结构、状态、内容一变,我前面那一大串祖先是不是都可能得重新算?
这就是为什么很多实现者一提 :has(),脑子里想到的不是“语法很优雅”,而是“失效范围会不会炸开”。
说得更土一点:
以前 CSS 匹配很多时候像就地判断。
:has() 很容易把它变成连带责任。
你改一个孩子,可能要重查一串家长。
而浏览器最怕的,就是这种“看起来很顺手,但会把重算范围悄悄放大”的东西。
04|所以 :has() 难的从来不是“想不出来”,而是“你敢不敢发”
这是这条线最关键的一点。
很多人会误以为,:has() 迟到是因为工作组以前没想到。
不是。
恰恰相反。
这类想法在 CSS 世界里存在太久了,久到它已经不是什么新鲜创意,而是一种近乎执念的公共愿望。
真正卡住它的,从来都不是“语法不会写”。
而是:
- 浏览器担心选择器匹配成本失控
- 浏览器担心 DOM 变化时的样式失效和重计算太重
- 浏览器担心一旦大规模放进真实页面,性能问题会以非常难看的方式爆出来
这和很多功能的拖延都不一样。
有些 CSS 特性是因为需求不大。
有些是因为路线争议太强。
:has() 的尴尬在于,它偏偏是:
需求极真,直觉极强,但工程侧长期不敢拍胸口说“我扛得住”。
这就导致它在标准史里处于一种特别折磨人的状态:
- 开发者永远在问“为什么还没有”
- 浏览器永远在说“你先别急”
- 双方都不是完全没道理
这类僵局,最容易一拖很多年。
05|这二十年的真正剧情,不是拒绝,而是长期不敢签收
如果你把 :has() 这条线写成“浏览器很保守,所以一直不肯给”,那就写浅了。
更准确的写法应该是:
浏览器并不是单纯反对这类能力,而是一直没有找到足够有把握的交付姿势。
这听起来像在替浏览器开脱。
但你把整个 Web 平台的历史放在一起看,就会发现这是个非常典型的模式。
浏览器团队真正怕的,从来不只是“做起来麻烦”。
他们更怕的是另一种事:
某个大家都很爱的能力,刚发货时像英雄,半年后却因为性能或兼容问题变成全网事故。
前缀时代已经证明过,一项半成熟能力只要足够受欢迎,就会被教程、框架、业务代码迅速固化成现实。
这时候你想回头再调,代价就会变得很难看。
所以 :has() 这么多年真正卡住的,不是“值不值得”。
而是:
谁能先站出来证明,这东西不只是优雅,而且跑得动。
没有这一步,它就会一直停在“人人想要,但谁也不敢先发”的区间里。
06|2022 年,WebKit 终于干了一件特别重要的事:不是宣布理想,而是宣布把账算平了
这就是为什么 Safari 15.4 的意义特别大。
WebKit 在 2022 年 3 月 发布关于 :has() 的文章时,传递出来的信息其实非常明确:
这不是“我们勉强放出来试试看”。
而更像是:
“我们已经把关键优化和实现路线想清楚了,所以现在可以认真把它交付给你。”
这一步很重要。
因为它让 :has() 这件事第一次从“多年愿望”变成“工程上可被证明的现实”。
更关键的是,WebKit 没有把这件事写成一句轻飘飘的“现在可以用了”。
它直接写到:团队为此做了 “novel :has-specific caching and filtering optimizations”,并结合了 CSS 引擎里已有的高级优化策略。
这句非常值钱。
因为它等于公开承认:
:has() 能上线,不是因为浏览器突然变勇敢了,而是因为终于有人把专门针对它的性能优化做出来了。
注意,这里真正有分量的,并不只是 Safari 抢先支持。
而是 Safari 作为第一个大规模主流实现,等于替整个行业做了一次证明:
这东西不是原则上不能做,而是需要有人把代价先啃下来。
这和很多标准史上的关键时刻都很像。
第一家真正把问题啃穿的,不只是发布了一个功能。
它还会顺带改写整个讨论气氛。
在这之前,大家谈的是:
“这会不会太贵?”
在这之后,大家谈的开始变成:
“既然有人已经把它做出来了,那别家什么时候跟?”
这就是立场变化。
一旦讨论从“能不能”切到“你什么时候上”,事情就已经不是原来那回事了。
07|Chrome 105 跟进的意义在于::has() 不再只是单引擎奇观
如果只有 WebKit 支持,:has() 仍然可以被看成一项漂亮但局部的胜利。
真正让它从“终于来了”升级成“前端心智真的开始改写”的,是 Chrome 105 在 2022 年 8 月 的跟进。
这件事的意义在于:
开发者终于开始把它当成一项现实能力,而不是技术新闻。
而且 Chromium 这边给出的公开材料,也比“我们支持了”更硬。
在 Blink 的 Intent to Ship 讨论里,团队直接把担忧写成了工程条目:
:has()可能增加 selector query 和 style invalidation 的复杂度- 为了压住风险,他们引入了单次操作级别的结果缓存
- 目标是让处理时间接近
O(n) - 内存消耗接近
O(log n) - 还专门讨论了哪些嵌套写法会让 invalidation complexity 继续恶化
这让 :has() 的历史一下就很清楚了。
浏览器不是没看到它的价值。
浏览器是在等一整套“我怎么不被它拖死”的答案。
一项 CSS 特性从“新闻事件”变成“可写进项目”的能力,中间差的从来不只是一个浏览器。
差的是一种社会心理门槛。
只有当大家觉得:
- 不再是孤例
- 不再像实验
- 不再只是某家浏览器的秀肌肉
它才会真正进入作者心智。
:has() 在 2022 年跨过去的,就是这道坎。
所以这条线最有意思的地方,不是“WebKit 赢了第一枪”“Chrome 很快跟上”这种赛马式叙事。
而是:
一个拖了二十年的公共愿望,终于从“原则上可想象”变成了“生态里可以认真依赖”。
这才是它真正的转折点。
08|为什么这件事特别能代表现代 CSS 的气质?
因为 :has() 很能说明,现代 CSS 里很多最受欢迎的能力,最后都不是靠“设计出语法”赢的。
而是靠“把性能恐惧处理到可接受”赢的。
这和外行对标准化的想象差别很大。
很多人以为标准化的难点在于概念争论、语法争论、委员会争论。
这些当然存在。
但到了 :has() 这种能力身上,你会发现真正拖时间的,往往是另一类更不显眼的东西:
- 匹配策略
- 失效范围
- 动态状态变化
- 大型页面下的最坏情况
这些都不容易写成戏剧化标题。
可它们恰恰决定了功能能不能活着进入生产世界。
这也是为什么 :has() 这条线特别有“江湖味”。
它表面上是一个选择器。
骨子里却是:
开发者直觉、规范愿望、引擎恐惧和工程耐心之间的一场长期拉扯。
09|如果你要给 :has() 下一个最准确的历史评价,它不是“终于补齐语法”,而是“终于有人敢为这份直觉买单”
:has() 不是那种靠愿景取胜的功能。
它能成功,正是因为它最后满足了现代 CSS 能活下来的那条铁律:
你不能只让作者觉得爽,你还得让浏览器觉得自己不会被拖死。
所以它这二十年最值得记住的,不是“前端等得好苦”。
而是:
浏览器花了二十年,才终于敢确认:这份大家都觉得理所当然的愿望,不会把平台压垮。
这是完全不同的视角。
也更接近标准史真正的残酷之处。
因为 Web 平台上很多被喊了很多年的需求,并不是死于没人喜欢。
它们往往死于:
没人愿意先为最坏情况签字。
:has() 的可贵,就在于它最后有人签了。
如果你只记一句,那就记这句:
:has() 难产二十年,不是因为 CSS 没想到“父选择器”,而是因为浏览器花了二十年,才终于敢确认自己背得动这笔性能账。
编者注(事实核对)::has() 属于 Selectors Level 4 中的关系型伪类讨论范畴,长期因性能与实现顾虑被推迟。WebKit 在官方文章中明确写到:过去 twenty years,开发者反复要求“parent selector”,但浏览器一直担心 复杂循环模式 与足够快的计算速度。WebKit 于 2022-03 随 Safari 15.4 公开发布 :has() 支持说明,并提到团队为此实现了 :has-specific caching and filtering optimizations;Chromium 于 2022-08 随 Chrome 105 跟进,Blink 的 Intent to Ship 材料中也明确提到单次操作缓存、接近 O(n) 的处理时间与接近 O(log n) 的内存目标,以及对 invalidation complexity 的限制讨论。文中将其概括为“父选择器愿望”“性能恐惧被慢慢啃下来的能力”,属于写作性归纳,不是规范原文措辞。
关键人物速览
- Jen Simmons:WebKit 阵营中把
:has()推向大众前端视野的重要传播者之一。她让很多人第一次从实际用法而不是纯规范角度理解这项能力。 - Tab Atkins Jr.:现代 CSS 模块与选择器讨论中的重要编辑人物,理解
:has()所属的更大 CSS 演进脉络时绕不开他。 - Igalia 团队:在现代 Web 平台一些艰难特性的跨引擎推进中经常出现。写
:has()的工程落地时,了解这类实现推动力量有助于避免把故事写成单一厂商神话。
参考与延伸阅读
Selectors Level 4
https://www.w3.org/TR/selectors-4/WebKit:Using :has() as a CSS Parent Selector and much more
https://webkit.org/blog/13096/css-has-pseudo-class/Chrome Developers:
:has()in Chrome 105
https://developer.chrome.com/blog/has-m105/Blink-dev:Intent to Ship
:has()pseudo class
https://groups.google.com/a/chromium.org/g/blink-dev/c/bRsbl3wLuykIgalia:How Blink tests
:has()pseudo class
https://blogs.igalia.com/blee/posts/2022/04/12/how-blink-tests-has-pseudo-class.htmlMDN:
:has()
https://developer.mozilla.org/en-US/docs/Web/CSS/:has
下篇预告:为什么 container queries 这个看起来几乎天经地义的需求,会在很长一段时间里被不少人直接判成“不可能”。